JavaScript 面向对象编程系列(二)继承

不像 Java,JavaScript 中并没有类的概念,因此要想实现继承,必须依靠 JavaScript 中的原型(prototype)机制。

继承的目的是使子类别具有父类别的属性和方法,子类别可以重新定义某些属性,也可以重写某些方法,子类别也可以添加新的属性和方法。

子类别可以继承父类别的属性和方法,但不是所有的属性和方法,因此我们做以下约定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Fun(){
// 私有成员
var val = ...; // 私有基本属性
var arr = ...; // 私有引用属性
function fun(){...} // 私有函数(方法)
// 实例成员
this.val = ...; // 实例基本属性
this.arr = ...; // 实例引用属性
this.fun = function(){...}; // 实例函数(方法)
}
// 原型成员
Fun.prototype.val = ...; // 原型基本属性
Fun.prototype.arr = ...; // 原型引用属性
Fun.prototype.fun = function(){...}; // 原型函数(方法)

只有父类别的实例成员才可以被继承,私有成员是不能被继承的。

接下来介绍实现继承的六种方法:构造函数继承、原型链继承、组合继承、原型式继承、寄生式继承、寄生组合式继承。

构造函数继承

实现原理: 将父对象的构造函数绑定在子对象上,父对象中的所有实例属性和方法复制到子对象,没有用到原型链。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Animal(age) {
this.species = "动物";
this.age = age;
this.sayAge = function() {
console.log(this.age);
}
}
function Dog(name,age) {
//Animal.call(this,age);
Animal.apply(this,[age]);
this.name = name;
this.sayName = function() {
console.log(this.name);
}
}
var teddy = new Dog("Teddy",7);
teddy.name; // "Teddy"
teddy.age; // 7
teddy.species; // "动物"
teddy.sayName(); // "Teddy"
teddy.sayAge(); // 7

在父对象 Animal 的构造函数中定义了 species 和 age 两个属性,和 sayAge 方法,在子对象 Dog 中调用 call() 或 apply() 方法使父对象的构造函数绑定到子对象上,子对象拥有父对象的所有实例属性和方法,Dog 对象的一个实例 teddy 也拥有其构造函数 Dog 上的所有属性和方法(包括 Dog 从 Animal 中继承过来的)。

优点:

  1. 子对象可以向父对象传递参数,如上述代码中的 age
  2. 可以实现多继承,即多次调用 call()/apply() 方法
  3. 父对象的属性和方法只是被复制,不被共享,避免了污染

缺点:

  1. 方法都是定义在构造函数中,无法被复用,继承时,每个子对象都有父对象方法的副本,造成内存的浪费
  2. 子对象只能继承父对象构造函数中的属性和方法,无法继承其原型对象中的属性和方法
1
2
3
4
5
Animal.prototype.sleep = function(){
console.log("sleep");
};
Animal.prototype.head = 1;

比如在父对象 Animal 的原型对象中添加一个 sleep 方法和 head 属性,子对象 Dog 的实例 teddy 是无法继承这些属性和方法的。

1
2
teddy.sleep(); // error
teddy.head; // undefined

但是在 Dog 的原型对象上定义的属性和方法,teddy 还是可以使用的,这主要还是因为 teddy 是 Dog 直接 new 出来的实例。

注意:借用构造函数实现的继承与原型链并没有关系。

1
2
3
4
5
6
7
Animal.prototype.__proto__; // Object.prototype
Dog.prototype.__proto__; // Object.prototype
teddy.__proto__; // Dog.prototype
// Animal.prototype --> Object.prototype
// Dog.prototype --> Object.prototype
// teddy --> Dog.prototype

可以看到父对象 Animal 和子对象 Dog 两者的原型对象之间并没有关联。

原型链继承

实现原理: 子类的原型对象指向父类的一个实例,实现原型链的连接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 父构造函数
function Animal() {}
// 父原型对象
Animal.prototype.sleep = function(){
console.log("sleep");
};
Animal.prototype.head = 1;
// 子构造函数
function Dog() {}
// 实现原型链继承
Dog.prototype = new Animal();
// 子原型对象
Dog.prototype.sound = function(){
console.log("汪汪");
};
Dog.prototype.feet = 4;
// 实例
var teddy = new Dog();
teddy.sleep(); // sleep
teddy.head; // 1
teddy.sound(); // "汪汪"
teddy.feet; // 4
Animal.prototype.__proto__; // Object.prototype
Dog.prototype.__proto__; // Animal.prototype
teddy.__proto__; // Dog.prototype
// teddy --> Dog.prototype --> Animal.prototype --> Object.prototype

只是单纯的使用 new 操作符创建一个父类实例:

1
Dog.prototype = new Animal();

会有一个问题:

1
Dog.prototype.constructor; // Animal

我们发现子类原型对象的 constructor 属性指向了父类构造函数。更重要的是,每一个实例也有一个 constructor属性,默认调用 prototype 对象的 constructor 属性。

1
2
teddy.constructor == Dog.prototype.constructor // true
teddy.constructor; // Animal

解决办法: 手动纠正,为 prototype 属性重新赋值。

1
2
3
// 实现原型链继承
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog;

优点:

  1. 实例可以继承子类和父类原型对象中的属性和方法
  2. 父类原型对象新增属性和方法,子类实例仍可以访问

缺点:

  1. 无法实现多继承
  2. 无法向父类构造函数传参
  3. 对子类原型的修改会反映到父类原型对象

组合继承

组合继承融合了原型链和构造函数的优点,是常用的继承模式。

实现原理: 使用原型链实现原型属性和方法(共享)的继承,使用构造函数实现对实例属性的继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 父构造函数
function Animal(name) {
this.name = name;
this.friends = ["Echo","Mike"];
}
// 父原型对象
Animal.prototype.color = ["black","white"]
Animal.prototype.sayName = function(name) {
console.log(this.name);
};
// 子构造函数
function Dog(name,age) {
// 借用构造函数继承
Animal.call(this,name); // 第二次调用
this.age = age;
}
// 实现原型链继承
Dog.prototype = new Animal(); // 第一次调用
Dog.prototype.constructor = Dog;
// 子原型对象
Dog.prototype.sayAge = function(){
console.log(this.age);
};
Dog.prototype.feet = 4;
// 实例
var teddy = new Dog("Teddy",3);
var samoyed = new Dog("Samoyed",7);
teddy.name; // "Teddy"
teddy.friends.push("Lily");
teddy.friends; // ["Echo","Mike","Lily"]
teddy.color.push("brown");
teddy.color; // ["black","white","brown"]
teddy.sayName(); // Teddy
teddy.age; // 3
teddy.sayAge(); // 3
teddy.feet; // 4
samoyed.name; // "Samoyed"
samoyed.friends; // ["Echo","Mike"]
samoyed.color; // ["black","white","brown"]
samoyed.sayName(); // Samoyed
samoyed.age; // 7
samoyed.sayAge(); // 7
samoyed.feet; // 4

优点:

  1. 子对象可以向父对象传递参数,可以实现多继承,继承自构造函数中的实例属性不会被污染
  2. 子对象共享继承自原型链中的属性和方法

缺点: 无论什么时候都会调用两次构造函数。

原型式继承

很多时候我们不一定会有或者说通过构造函数实现继承,可能我们只有两个对象,我们想实现这两个对象的继承。本例参考阮一峰老师的博客

1
2
3
4
5
6
var Chinese = {
nation: "中国"
}
var Doctor = {
career: "医生"
}

json 的发明人提出一个 object() 函数:

1
2
3
4
5
function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

在 object() 函数内部,先创建了一个临时的构造函数,将传入对象作为该构造函数的原型对象,最后返回该构造函数的一个实例。实际上就是父对象传入 object() 函数,返回子对象,继承关系还是通过原型链实现的。

1
2
var Doctor = object(Chinese);
Doctor.nation; // "中国"

同样的,在父对象的属性和方法的任何修改会反映到子对象上。

1
2
Chinese.age = 18;
Doctor.age; // 18

寄生式继承

寄生式继承创建个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function createAnother(original){
var clone = Object.create(original); // 通过调用函数创建一个新对象
clone.sayHi = function() { // 以某种方式来增强这个对象
console.log("Hi");
};
return clone; // 返回这个对象
}
var person = {
name: "Bob",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // "Hi"
person.sayHi(); // 报错
anotherPerson.friends.push("Echo");
anotherPerson.friends; // ["Shelby", "Court", "Van", "Echo"]
person.friends; // ["Shelby", "Court", "Van", "Echo"]

createAnother() 函数接收了一个参数,即被继承对象。

anotherPerson 是基于 person 创建的一个新对象,新对象不仅具有person的所有属性和方法,还有自己的 sayHi() 方法。但是父对象的引用属性是共享的。

寄生组合式继承

寄生组合式继承主要是对组合继承的改进,在组合继承中,最大的问题就是无论什么时候都会调用两次构造函数。

1
2
3
4
5
6
7
8
9
10
// 子构造函数
function Dog(name,age) {
// 借用构造函数继承
Animal.call(this,name); // 第二次调用Animal()
this.age = age;
}
// 实现原型链继承
Dog.prototype = new Animal(); // 第一次调用Animal()
Dog.prototype.constructor = Dog;

为了解决这一问题,寄生组合式继承 通过借用构造函数来继承属性,通过原型链来继承方法。

Fork me on GitHub